// HTMLParser Library - A java-based parser for HTML
// http://htmlparser.org
// Copyright (C) 2006 Derrick Oswald
//
// Revision Control Information
//
// $URL: https://svn.sourceforge.net/svnroot/htmlparser/trunk/filterbuilder/src/main/java/org/htmlparser/parserapplications/filterbuilder/Filter.java $
// $Author: derrickoswald $
// $Date: 2006-09-17 21:02:25 -0400 (Sun, 17 Sep 2006) $
// $Revision: 8 $
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the Common Public License; either
// version 1.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// Common Public License for more details.
//
// You should have received a copy of the Common Public License
// along with this library; if not, the license is available from
// the Open Source Initiative (OSI) website:
//   http://opensource.org/licenses/cpl1.0.php

package org.htmlparser.parserapplications.filterbuilder;

import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.*;
import javax.swing.border.*;

import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.parserapplications.filterbuilder.layouts.VerticalLayoutManager;

/**
 * Base class for all filters. Provides common functionality applicable to all
 * filters.
 */
public abstract class Filter extends JComponent implements NodeFilter {
	/**
	 * Create a new filter from the class name.
	 * 
	 * @param class_name
	 *            The class to instatiate.
	 * @return The constructed filter object.
	 */
	public static Filter instantiate(String class_name) {
		Filter ret;

		ret = null;
		try {
			Class cls = Class.forName(class_name);
			ret = (Filter) cls.newInstance();
			mWrappers.put(ret.getNodeFilter().getClass().getName(), class_name);
		} catch (ClassNotFoundException cnfe) {
			System.out.println("can't find class " + class_name);
		} catch (InstantiationException ie) {
			System.out.println("can't instantiate class " + class_name);
		} catch (IllegalAccessException ie) {
			System.out.println("class " + class_name + " has no public constructor");
		} catch (ClassCastException cce) {
			System.out.println("class " + class_name + " is not a Filter");
		}

		return (ret);
	}

	/**
	 * Map from cilter class to wrapper. Populated as part of each wrapper being
	 * loaded.
	 */
	protected static Hashtable mWrappers = new Hashtable();

	/**
	 * Line separator string.
	 */
	protected static String mNewLine = System.getProperty("line.separator");

	/**
	 * Create a filter. Set up the default display. Only a border with the label
	 * of the filter name, returned by <code>getDescription()</code>, and an
	 * icon, returned by <code>getIcon()</code>.
	 */
	public Filter() {
		JLabel label;
		Dimension dimension;
		Insets insets;

		setToolTipText(getDescription());
		// none of these quite does it:
		// new BoxLayout (this, BoxLayout.Y_AXIS));
		// new GridLayout (0, 1));
		setLayout(new VerticalLayoutManager());
		setSelected(false);
		label = new JLabel(getDescription(), getIcon(), SwingConstants.LEFT);
		label.setBackground(Color.green);
		label.setAlignmentX(Component.LEFT_ALIGNMENT);
		label.setHorizontalAlignment(SwingConstants.LEFT);
		add(label);
		dimension = label.getMaximumSize();
		insets = getInsets();
		dimension.setSize(dimension.width + insets.left + insets.right, dimension.height + insets.top + insets.bottom);
		setSize(dimension);
	}

	/**
	 * Get the name of the filter.
	 * 
	 * @return A descriptive name for the filter.
	 */
	public abstract String getDescription();

	/**
	 * Get the underlying node filter object.
	 * 
	 * @return The node filter object suitable for serialization.
	 */
	public abstract NodeFilter getNodeFilter();

	/**
	 * Assign the underlying node filter for this wrapper.
	 * 
	 * @param filter
	 *            The filter to wrap.
	 * @param context
	 *            The parser to use for conditioning this filter. Some filters
	 *            need contextual information to provide to the user, i.e. for
	 *            tag names or attribute names or values, so the Parser context
	 *            is provided.
	 */
	public abstract void setNodeFilter(NodeFilter filter, Parser context);

	/**
	 * Get the underlying node filter's subordinate filters.
	 * 
	 * @return The node filter object's contained filters.
	 */
	public abstract NodeFilter[] getSubNodeFilters();

	/**
	 * Assign the underlying node filter's subordinate filters.
	 * 
	 * @param filters
	 *            The filters to insert into the underlying node filter.
	 */
	public abstract void setSubNodeFilters(NodeFilter[] filters);

	/**
	 * Convert this filter into Java code. Output whatever text necessary and
	 * return the variable name.
	 * 
	 * @param out
	 *            The output buffer.
	 * @param context
	 *            Three integers as follows:
	 *            <li>indent level - the number of spaces to insert at the
	 *            beginning of each line</li>
	 *            <li>filter number - the next available filter number</li>
	 *            <li>filter array number - the next available array of filters
	 *            number</li>
	 * @return The variable name to use when referencing this filter (usually
	 *         "filter" + context[1]++)
	 */
	public abstract String toJavaCode(StringBuffer out, int[] context);

	/**
	 * Get the icon for the filter. Loads the resource specified by
	 * <code>getIconSpec()</code> as an icon.
	 * 
	 * @return The icon or null if it was not found.
	 */
	public Icon getIcon() {
		ImageIcon ret;

		ret = null;
		try {
			ret = new ImageIcon(getClass().getResource(getIconSpec()));
		} catch (NullPointerException npe) {
			System.err.println("can't find icon " + getIconSpec());
		}

		return (ret);
	}

	/**
	 * Get the resource name for the icon.
	 * 
	 * @return The icon resource specification.
	 */
	public abstract String getIconSpec();

	//
	// Component overrides
	//

	/**
	 * Returns a string representation of this component and its values.
	 * 
	 * @return A string representation of this component.
	 */
	public String toString() {
		return (getDescription() + " [" + this.getClass().getName() + "]");
	}

	//
	// utilities
	//

	/**
	 * Serialize an object to a byte array.
	 * 
	 * @param object
	 *            The object to be pickled.
	 * @return The serialized object.
	 * @exception IOException
	 *                If the output stream complains (unlikely).
	 */
	public static byte[] pickle(Object object) throws IOException {
		ByteArrayOutputStream bos;
		ObjectOutputStream oos;
		byte[] ret;

		bos = new ByteArrayOutputStream();
		oos = new ObjectOutputStream(bos);
		oos.writeObject(object);
		oos.close();
		ret = bos.toByteArray();

		return (ret);
	}

	/**
	 * Reconstitute a serialized object.
	 * 
	 * @param data
	 *            The pickled object.
	 * @return The reconstituted object.
	 * @exception IOException
	 *                If the input stream complains.
	 * @exception ClassNotFoundException
	 *                If the serialized object class cannot be located.
	 */
	public static Object unpickle(byte[] data) throws IOException, ClassNotFoundException {
		ByteArrayInputStream bis;
		ObjectInputStream ois;
		Object ret;

		bis = new ByteArrayInputStream(data);
		ois = new ObjectInputStream(bis);
		ret = ois.readObject();
		ois.close();

		return (ret);
	}

	/**
	 * Serialize a byte array to a String. Convert each byte from the serialized
	 * object into a couple of hexadecimal characters.
	 * 
	 * @param data
	 *            The serialized object as a byte array.
	 * @return The string representing the serialized object.
	 */
	public static String serialize(byte[] data) {
		String string;
		StringBuffer ret;

		ret = new StringBuffer(data.length * 2);

		for (int i = 0; i < data.length; i++) {
			string = Integer.toString(0xff & data[i], 16);
			if (string.length() < 2)
				ret.append("0");
			ret.append(string);
		}

		return (ret.toString());
	}

	/**
	 * Convert a sequence of hexadecimal characters back into a byte array.
	 * 
	 * @param string
	 *            The string to convert (must be correct hex characters).
	 * @return The bytes as an array.
	 */
	public static byte[] deserialize(String string) {
		byte[] ret;

		ret = new byte[string.length() / 2];

		for (int i = 0; i < string.length(); i += 2)
			ret[i / 2] = (byte) Integer.parseInt(string.substring(i, i + 2), 16); // todo:
																					// hopelessly
																					// inefficient

		return (ret);
	}

	/**
	 * Returns a string serialization of the filters.
	 * 
	 * @param filters
	 *            The list of filters to serialize.
	 * @return A string representation of the filters.
	 * @exception IOException
	 *                If serialization fails.
	 */
	public static String deconstitute(Filter[] filters) throws IOException {
		StringBuffer ret;

		ret = new StringBuffer(1024);
		for (int i = 0; i < filters.length; i++) {
			ret.append("[");
			ret.append(serialize(pickle(filters[i].getNodeFilter())));
			ret.append("]");
		}

		return (ret.toString());
	}

	/**
	 * Returns the filters represented by the string.
	 * 
	 * @param string
	 *            The string with serialized node filters.
	 * @param context
	 *            The context from which to extract meaningful values for GUI
	 *            choices (which aren't serialized).
	 * @return The filters gleaned from the string.
	 * @see #wrap
	 */
	public static Filter[] reconstitute(String string, Parser context) {
		Filter[] ret;
		Vector vector;
		int index;
		String code;
		Object object;
		Filter filter;

		vector = new Vector();
		try {
			while (string.startsWith("[")) {
				index = string.indexOf(']');
				if (-1 != index) {
					code = string.substring(1, index);
					string = string.substring(index + 1);
					object = unpickle(deserialize(code));
					if (object instanceof NodeFilter) {
						filter = wrap((NodeFilter) object, context);
						if (null != filter)
							vector.addElement(filter);
					} else
						break;
				} else
					break;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		ret = new Filter[vector.size()];
		vector.copyInto(ret);

		return (ret);
	}

	/**
	 * Get the enclosed sub filter list if any. Todo: rationalize with
	 * FilterBuilder's method(s) of the same name.
	 * 
	 * @param component
	 *            The component that's supposedly enclosing the list.
	 * @return The enclosed component or <code>null</code> otherwise.
	 */
	protected static SubFilterList getEnclosed(Component component) {
		Component[] list;

		if (component instanceof Container) {
			list = ((Container) component).getComponents();
			for (int i = 0; i < list.length; i++)
				if (list[i] instanceof SubFilterList)
					return ((SubFilterList) list[i]);
		}

		return (null);
	}

	/**
	 * Returns a wrapped filter.
	 * 
	 * @param filter
	 *            A filter to be wrapped by GUI components.
	 * @param context
	 *            The context within which to wrap the object. Some wrappers
	 *            need context to set up useful choices for the user.
	 * @return The filter to wrap.
	 */
	public static Filter wrap(NodeFilter filter, Parser context) {
		String class_name;
		NodeFilter[] filters;
		SubFilterList list;
		Filter ret;

		ret = null;

		class_name = filter.getClass().getName();
		class_name = (String) mWrappers.get(class_name);
		if (null != class_name) {
			try {
				ret = Filter.instantiate(class_name);
				ret.setNodeFilter(filter, context);
				// recurse into subfilters
				filters = ret.getSubNodeFilters();
				if (0 != filters.length) {
					list = getEnclosed(ret);
					if (null != list) {
						ret.setSubNodeFilters(new NodeFilter[0]); // clean out
																	// the
																	// unwrapped
																	// filters
						for (int i = 0; i < filters.length; i++)
							list.addFilter(wrap(filters[i], context));
					} else
						throw new IllegalStateException("filter can't have subnodes without a SubFilterList on the wrapper");
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} else
			System.out.println(class_name + " is not registered for wrapping.");

		return (ret);
	}

	/**
	 * Set the 'selected look' for the component.
	 * 
	 * @param selected
	 *            If <code>true</code>, 'select' this component, otherwise
	 *            'deselect' it.
	 */
	public void setSelected(boolean selected) {
		if (selected)
			setBorder(new CompoundBorder(new EtchedBorder(), new CompoundBorder(new LineBorder(Color.blue, 2), new EmptyBorder(1, 1, 1, 1))));
		else
			setBorder(new CompoundBorder(new EtchedBorder(), new EmptyBorder(3, 3, 3, 3)));
	}

	/**
	 * Set the expanded state for the component. This sets invisible all but the
	 * JLabel component in the comand component.
	 * 
	 * @param expanded
	 *            If <code>true</code>, 'expand' this component, otherwise
	 *            'collapse' it.
	 */
	public void setExpanded(boolean expanded) {
		Component[] components;

		components = getComponents();
		for (int i = 0; i < components.length; i++)
			if (!(components[i] instanceof JLabel))
				components[i].setVisible(expanded);
	}

	/**
	 * Append count spaces to the buffer.
	 * 
	 * @param out
	 *            The buffer to append to.
	 * @param count
	 *            The number of spaces to append.
	 */
	public static void spaces(StringBuffer out, int count) {
		for (int i = 0; i < count; i++)
			out.append(' ');
	}

	/**
	 * Append a newline to the buffer.
	 * 
	 * @param out
	 *            The buffer to append to.
	 */
	public static void newline(StringBuffer out) {
		out.append(mNewLine);
	}
}
